package reader;

import document.AttributeSet;
import document.HtmlDocument;
import exception.InvalidHtmlDocumentException;
import treemodel.Node;
import treemodel.Container;
import treemodel.container.general.*;
import treemodel.container.wrapped.*;
import treemodel.Line;
import treemodel.line.*;
import treemodel.table.Table;
import treemodel.table.TableRow;
import treemodel.unit.ImageUnit;
import treemodel.unit.TextUnit;

import java.util.HashMap;
import java.util.Stack;

/**
 * Created by Pavel Nikolaev
 */
public class ModelBuilder extends HtmlReader {
    private final static char ourTagSubstitutionSymbol = (char)0x1F; // US - unit separator

    private String myText;
    private String myTitle;
    private Node myRoot;

    private boolean myTitleMode;
    private boolean myIgnoreMode;

    private Line myCurrentUnitLine;
    private Container myCurrentContainer;
    private Stack<Container> myContainerStack;

    private AttributeSet myCurrentAttributeSet;
    private Stack<HtmlTag> myTagStack;
    private HtmlTag myCurrentTag;
    private HashMap<HtmlTag.TagType, HtmlTagAction> myActions;


    public ModelBuilder() {
        myText = "";
        myTitle = "";
        myRoot = null;

        myTitleMode = false;
        myIgnoreMode = false;

        myCurrentUnitLine = null;
        myCurrentContainer = null;
        myContainerStack = new Stack<Container>();

        myCurrentAttributeSet = new AttributeSet(0);
        myTagStack = new Stack<HtmlTag>();
        myCurrentTag = null;

        myActions = new HashMap<HtmlTag.TagType, HtmlTagAction>();

        this.myActions.put(HtmlTag.TagType.HEAD, new HtmlTagAction() {
            public void run(HtmlTag tag) {
                myIgnoreMode = tag.isOpen;
            }
        });
        this.myActions.put(HtmlTag.TagType.TITLE, new HtmlTagAction() {
            public void run(HtmlTag tag) {
                myTitleMode = tag.isOpen;
            }
        });
        this.myActions.put(HtmlTag.TagType.B, new HtmlTagAction() {
            public void run(HtmlTag tag) {
                myCurrentAttributeSet.myBoldMode = tag.isOpen;
            }
        });
        this.myActions.put(HtmlTag.TagType.I, new HtmlTagAction() {
            public void run(HtmlTag tag) {
                myCurrentAttributeSet.myItalicMode = tag.isOpen;
            }
        });
        this.myActions.put(HtmlTag.TagType.A, new HtmlTagAction() {
            public void run(HtmlTag tag) {
                myCurrentAttributeSet.myUnderlinedMode = tag.isOpen;
            }
        });
        this.myActions.put(HtmlTag.TagType.HTML, new TagCheckAction());
        this.myActions.put(HtmlTag.TagType.BODY, new BodyAction());
        this.myActions.put(HtmlTag.TagType.H1, new HeaderAction(1));
        this.myActions.put(HtmlTag.TagType.H2, new HeaderAction(2));
        this.myActions.put(HtmlTag.TagType.H3, new HeaderAction(3));
        this.myActions.put(HtmlTag.TagType.H4, new HeaderAction(4));
        this.myActions.put(HtmlTag.TagType.H5, new HeaderAction(5));
        this.myActions.put(HtmlTag.TagType.H6, new HeaderAction(6));
        this.myActions.put(HtmlTag.TagType.PRE, new PreformattedParagraphAction());
        this.myActions.put(HtmlTag.TagType.P, new ParagraphAction());
        this.myActions.put(HtmlTag.TagType.TABLE, new TableAction());
        this.myActions.put(HtmlTag.TagType.TR, new TableRowAction());
        this.myActions.put(HtmlTag.TagType.TD, new TableCellAction());
        this.myActions.put(HtmlTag.TagType.HR, new HorizontalRuleAction());
        this.myActions.put(HtmlTag.TagType.BR, new BreakLineAction());
        this.myActions.put(HtmlTag.TagType.UL, new UnorderedListAction());
        this.myActions.put(HtmlTag.TagType.OL, new OrderedListAction());
        this.myActions.put(HtmlTag.TagType.LI, new ListInstanceAction());
        this.myActions.put(HtmlTag.TagType.IMG, new ImageAction());
    }

    public Node getRoot() {
        return myRoot;
    }

    public HtmlDocument getDocument() {
        return new HtmlDocument(myTitle, myRoot, myText, ourTagSubstitutionSymbol);
    }

    private interface HtmlTagAction {
        public void run(HtmlTag tag) throws InvalidHtmlDocumentException;
    }

    private class TagCheckAction implements HtmlTagAction {
        public void run(HtmlTag tag) throws InvalidHtmlDocumentException {
            if (tag.isOpen) {
                myTagStack.push(myCurrentTag);
                myCurrentTag = tag;
            } else if (myCurrentTag.type == null) {
                throw new InvalidHtmlDocumentException("No any opening tag for the tag: " + tag.name);
            } else if (myCurrentTag.type == tag.type) {
                myCurrentTag = myTagStack.pop();
            } else {
                throw new InvalidHtmlDocumentException("Trial to close tag " + myCurrentTag.name + " with " + tag.name);
            }

        }
    }

    private abstract class ContainerTagAction implements HtmlTagAction {
        public void run(HtmlTag tag) throws InvalidHtmlDocumentException {
            if (tag.isOpen) {
                myCurrentUnitLine = null;
                Container container = create();
                myCurrentContainer.add(container);
                myContainerStack.push(myCurrentContainer);
                myCurrentContainer = container;
                myTagStack.push(myCurrentTag);
                myCurrentTag = tag;
            } else if (myCurrentTag == null) {
                throw new InvalidHtmlDocumentException("No opening tag for tag: " + tag.name);
            } else if (myCurrentTag.type == tag.type) {
                myCurrentUnitLine = null;
                myCurrentContainer = myContainerStack.pop();
                myCurrentTag = myTagStack.pop();
            } else {
                throw new InvalidHtmlDocumentException("Trial to close tag " + myCurrentTag.name + " with " + tag.name);
            }
        }

        protected abstract Container create();
    }

    private final class BodyAction extends TagCheckAction {
        @Override
        public void run(HtmlTag tag) throws InvalidHtmlDocumentException {
            if (tag.isOpen) {
                myCurrentContainer = new Body();
                myRoot = myCurrentContainer;
            }
            super.run(tag);
        }
    }

    private final class PreformattedParagraphAction extends ContainerTagAction {
        @Override
        public void run(HtmlTag tag) throws InvalidHtmlDocumentException {
            super.run(tag);
            myCurrentAttributeSet.myPreformattedMode = tag.isOpen;
        }

        @Override
        protected Container create() {
            return new PreformattedParagraph();
        }
    }

    private final class ParagraphAction extends ContainerTagAction {
        @Override
        protected Container create() {
            return new Paragraph();
        }
    }

    private final class TableAction extends ContainerTagAction {
        @Override
        protected Container create() {
            return new Table();
        }
    }

    private final class TableRowAction extends ContainerTagAction {
        @Override
        protected Container create() {
            return new TableRow();
        }
    }

    private final class TableCellAction extends ContainerTagAction {
        @Override
        protected Container create() {
            return new TableCell();
        }
    }


    private final class UnorderedListAction extends ContainerTagAction {
        @Override
        protected Container create() {
            return new MarkedList(false);
        }
    }

    private final class OrderedListAction extends ContainerTagAction {
        @Override
        protected Container create() {
            return new MarkedList(true);
        }
    }

    private final class ListInstanceAction extends ContainerTagAction {
        @Override
        protected Container create() {
            return new ListInstance();
        }
    }

    private final class HorizontalRuleAction implements HtmlTagAction {
        public void run(HtmlTag tag) {
            myCurrentContainer.add(new HorizontalRuleLine());
            myCurrentUnitLine = null;
        }
    }

    private final class BreakLineAction implements HtmlTagAction {
        public void run(HtmlTag tag) {
            if (myCurrentUnitLine == null)
                myCurrentContainer.add(new EmptyLine());
            else
                myCurrentUnitLine = null;
        }
    }


    private final class ImageAction implements HtmlTagAction {
        public void run(HtmlTag tag) throws InvalidHtmlDocumentException {
            if (myCurrentUnitLine == null) {
                myCurrentUnitLine = myCurrentContainer.createLine();
                myCurrentContainer.add(myCurrentUnitLine);
            }
            myCurrentUnitLine.add(new ImageUnit(tag));
        }
    }

    private final class HeaderAction extends ContainerTagAction {
        private int myLevel;

        public HeaderAction(int level) {
            myLevel = level;
        }

        @Override
        public void run(HtmlTag tag) throws InvalidHtmlDocumentException {
            super.run(tag);
            myCurrentAttributeSet.mySizeLevel = (tag.isOpen) ? myLevel : 0;
        }

        @Override
        protected Container create() {
            return new Header();
        }
    }

    @Override
    protected void handleDocumentStart() {
        myCurrentTag = new HtmlTag(HtmlTag.TagType.FAKE_TAG);
    }

    @Override
    protected void handleDocumentEnd() throws InvalidHtmlDocumentException {
        if ((myCurrentTag == null) || (myCurrentTag.type != HtmlTag.TagType.FAKE_TAG)) {
            String msg = "Incorrect tag sequence. The structure of html ";
            msg += "contains unclosed tags.";
            throw new InvalidHtmlDocumentException(msg);
        }
    }

    @Override
    protected void handleTag(HtmlTag tag) throws InvalidHtmlDocumentException {
        // Tag substitution symbol adding
        if (myText.isEmpty()) {
            myText += ourTagSubstitutionSymbol;
        } else {
            final char lastChar = myText.charAt(myText.length() - 1);
            if (lastChar != ourTagSubstitutionSymbol)
                myText += ourTagSubstitutionSymbol;
        }
        // Tag processing
        HtmlTagAction action = myActions.get(tag.type);
        if (action != null) action.run(tag);
    }

    @Override
    protected void handleText(String text) throws InvalidHtmlDocumentException {
        if (text.isEmpty()) return;
        if (myTitleMode) {
            myTitle = text;
        } else if (!myIgnoreMode) {
            if (myCurrentUnitLine == null) {
                if (!myCurrentAttributeSet.myPreformattedMode) {
                    if (text.charAt(0) == ' ') text = text.substring(1);
                    if (text.isEmpty()) return;
                }
                myCurrentUnitLine = myCurrentContainer.createLine();
                myCurrentContainer.add(myCurrentUnitLine);
            }
            int offset = myText.length();
            myText += text;
            TextUnit unit = new TextUnit(myText, offset, text.length(), myCurrentAttributeSet.clone());
            myCurrentUnitLine.add(unit);
        }
    }
}
